Skip to content

Fix drag selection freezing over the gutter#126

Open
BetaYao wants to merge 1 commit into
CodeEditApp:mainfrom
BetaYao:fix/drag-selection-over-gutter
Open

Fix drag selection freezing over the gutter#126
BetaYao wants to merge 1 commit into
CodeEditApp:mainfrom
BetaYao:fix/drag-selection-over-gutter

Conversation

@BetaYao

@BetaYao BetaYao commented Jul 27, 2026

Copy link
Copy Markdown

Description

mouseDragged clamps the pointer into the view before asking the layout manager for a text offset:

let locationInView = CGPoint(
    x: max(0.0, min(locationInWindow.x, frame.width)),
    y: max(0.0, min(locationInWindow.y, frame.height))
)

The lower bound is wrong. TextLayoutManager.textOffsetAtPoint(_:) resolves positions against edgeInsets.left — it calls fragment.findContent(atX: xPos - edgeInsets.left), and findContent walks fragments starting at xPos: 0, so any negative value returns nil. The resolvable region starts at edgeInsets.left, not at 0.

Clamping to 0 therefore leaves a dead band of exactly edgeInsets.left points on the leading edge. That band is not hypothetical: CodeEditSourceEditor sets textInsets.left = gutterView.frame.width, so it is the full width of the gutter, ~45–60pt.

When the pointer enters that band mid-drag, textOffsetAtPoint returns nil, the guard fails, and mouseDragged returns early — dropping the entire event. The selection stops updating, and autoscroll(with:) never runs either, since it sits after the guard.

The user-visible effect is that dragging up and to the left freezes the selection at the last sample taken inside the text area — typically a few characters into the top line, or a whole line short if the drag was quick. Moving the pointer further up along the left margin does nothing at all.

Fix: extract the clamping into clampToTextArea(_:) and clamp into [edgeInsets.left, frame.width - edgeInsets.right]. Positions over the gutter now map to the start of a line, which matches what NSTextView does.

Also in this PR: mouseDragAnchor was set on the first mouseDragged rather than on mouseDown, so the selection anchored wherever the pointer had already traveled to by the time that event arrived — a character or two from where the user actually clicked on a quick drag. It is now set in mouseDown. The mouseDragAnchor == nil branch in mouseDragged is kept as a fallback for paths that don't go through the click handlers. This is safe with respect to drag-and-drop of selected text, which is driven by the separate DragSelectionGesture recognizer in TextView+Drag.swift and does not depend on the super.mouseDragged(with:) call in that branch.

I'm happy to split the anchor change into its own PR if you'd prefer — they're both one-line behavioral fixes in the same drag path, so I kept them together.

To Reproduce

  1. Open a document in CodeEditSourceEditor (or any host that sets a non-zero edgeInsets.left) with the gutter visible.
  2. Press the mouse down near the end of line 4 and drag up and to the left toward the start of line 1, passing over the gutter.
  3. The selection stops updating as soon as the pointer crosses into the gutter. The first characters of the top line are never selected.

Related Issues

This is the residue of #100 and CodeEditApp/CodeEditSourceEditor#316. #108 fixed the right edge and the vertical bounds, but the leading edge still bails out, so #316 ("Gutter Intercepts TextView Drag") is only partially fixed — the dead band shrank from "the gutter plus everything right of the view" to "the gutter".

Tests

Added TextViewMouseTests covering:

  • a point over the leading inset resolves to the start of a line (with a precondition asserting the raw, unclamped point is unresolvable — this fails without the fix)
  • a point left of the view entirely clamps the same way
  • a point right of the view clamps inside the trailing inset
  • a point already inside the text area is returned unchanged
  • vertical positions clamp to the view
  • insets wider than the view don't produce an inverted range
  • mouseDown sets a resolvable drag anchor

The existing suite (72 tests) passes unchanged.

Checklist

  • I read and understood the contributing guide as well as the code of conduct
  • The issues this PR addresses are related to each other
  • My changes generate no new warnings
  • My code builds and runs on my machine
  • My changes are all related to the related issue above
  • I documented my code

Screenshots

Not attached — the bug is a pointer-tracking behavior rather than a rendering issue, so the repro steps above are more useful than a still. Happy to record a screen capture if that helps.

`mouseDragged` clamped the pointer to `max(0, ...)`, but the layout manager
can only resolve offsets at or right of `edgeInsets.left` — the strip a
gutter is drawn over. Any position further left made `textOffsetAtPoint`
return nil, which tripped the guard and returned early, so the whole drag
event was dropped: the selection stopped updating (and autoscroll stopped
firing) for as long as the pointer sat over the gutter.

Dragging up and to the left therefore stranded the selection at the last
sample taken inside the text area, typically a few characters into the top
line. This is the residue of CodeEditApp/CodeEditSourceEditor#316 and CodeEditApp#100,
which fixed the right edge but not the left.

Clamp into the inset region instead, so positions over the gutter map to the
start of a line. Also anchor the drag on mouse down rather than on the first
drag event, which otherwise starts the selection wherever the pointer had
already traveled to.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant